Passed
Branch wavefile-rw (44886c)
by Rafael S.
07:26
created

WaveFileParser.getCuePointsBytes_   A

Complexity

Conditions 2

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 14
rs 9.85
c 0
b 0
f 0
cc 2
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileParser class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import {encode, decode} from 'base64-arraybuffer-es6';
31
import RIFFFile from './riff-file';
32
import writeString from './write-string';
33
import fixRIFFTag from './fix-riff-tag';
34
import {packTo, packStringTo, packString, pack} from 'byte-data';
35
36
/**
37
 * A class to read and write wav files.
38
 */
39
export default class WaveFileParser extends RIFFFile {
40
41
  constructor() {
42
    super();
43
    /**
44
     * Audio formats.
45
     * Formats not listed here should be set to 65534,
46
     * the code for WAVE_FORMAT_EXTENSIBLE
47
     * @enum {number}
48
     * @private
49
     */
50
    this.WAV_AUDIO_FORMATS = {
51
      '4': 17,
52
      '8': 1,
53
      '8a': 6,
54
      '8m': 7,
55
      '16': 1,
56
      '24': 1,
57
      '32': 1,
58
      '32f': 3,
59
      '64': 3
60
    };
61
    /**
62
     * The data of the 'fmt' chunk.
63
     * @type {!Object<string, *>}
64
     */
65
    this.fmt = {
66
      /** @type {string} */
67
      chunkId: '',
68
      /** @type {number} */
69
      chunkSize: 0,
70
      /** @type {number} */
71
      audioFormat: 0,
72
      /** @type {number} */
73
      numChannels: 0,
74
      /** @type {number} */
75
      sampleRate: 0,
76
      /** @type {number} */
77
      byteRate: 0,
78
      /** @type {number} */
79
      blockAlign: 0,
80
      /** @type {number} */
81
      bitsPerSample: 0,
82
      /** @type {number} */
83
      cbSize: 0,
84
      /** @type {number} */
85
      validBitsPerSample: 0,
86
      /** @type {number} */
87
      dwChannelMask: 0,
88
      /**
89
       * 4 32-bit values representing a 128-bit ID
90
       * @type {!Array<number>}
91
       */
92
      subformat: []
93
    };
94
    /**
95
     * The data of the 'fact' chunk.
96
     * @type {!Object<string, *>}
97
     */
98
    this.fact = {
99
      /** @type {string} */
100
      chunkId: '',
101
      /** @type {number} */
102
      chunkSize: 0,
103
      /** @type {number} */
104
      dwSampleLength: 0
105
    };
106
    /**
107
     * The data of the 'cue ' chunk.
108
     * @type {!Object<string, *>}
109
     */
110
    this.cue = {
111
      /** @type {string} */
112
      chunkId: '',
113
      /** @type {number} */
114
      chunkSize: 0,
115
      /** @type {number} */
116
      dwCuePoints: 0,
117
      /** @type {!Array<!Object>} */
118
      points: [],
119
    };
120
    /**
121
     * The data of the 'smpl' chunk.
122
     * @type {!Object<string, *>}
123
     */
124
    this.smpl = {
125
      /** @type {string} */
126
      chunkId: '',
127
      /** @type {number} */
128
      chunkSize: 0,
129
      /** @type {number} */
130
      dwManufacturer: 0,
131
      /** @type {number} */
132
      dwProduct: 0,
133
      /** @type {number} */
134
      dwSamplePeriod: 0,
135
      /** @type {number} */
136
      dwMIDIUnityNote: 0,
137
      /** @type {number} */
138
      dwMIDIPitchFraction: 0,
139
      /** @type {number} */
140
      dwSMPTEFormat: 0,
141
      /** @type {number} */
142
      dwSMPTEOffset: 0,
143
      /** @type {number} */
144
      dwNumSampleLoops: 0,
145
      /** @type {number} */
146
      dwSamplerData: 0,
147
      /** @type {!Array<!Object>} */
148
      loops: []
149
    };
150
    /**
151
     * The data of the 'bext' chunk.
152
     * @type {!Object<string, *>}
153
     */
154
    this.bext = {
155
      /** @type {string} */
156
      chunkId: '',
157
      /** @type {number} */
158
      chunkSize: 0,
159
      /** @type {string} */
160
      description: '', //256
161
      /** @type {string} */
162
      originator: '', //32
163
      /** @type {string} */
164
      originatorReference: '', //32
165
      /** @type {string} */
166
      originationDate: '', //10
167
      /** @type {string} */
168
      originationTime: '', //8
169
      /**
170
       * 2 32-bit values, timeReference high and low
171
       * @type {!Array<number>}
172
       */
173
      timeReference: [0, 0],
174
      /** @type {number} */
175
      version: 0, //WORD
176
      /** @type {string} */
177
      UMID: '', // 64 chars
178
      /** @type {number} */
179
      loudnessValue: 0, //WORD
180
      /** @type {number} */
181
      loudnessRange: 0, //WORD
182
      /** @type {number} */
183
      maxTruePeakLevel: 0, //WORD
184
      /** @type {number} */
185
      maxMomentaryLoudness: 0, //WORD
186
      /** @type {number} */
187
      maxShortTermLoudness: 0, //WORD
188
      /** @type {string} */
189
      reserved: '', //180
190
      /** @type {string} */
191
      codingHistory: '' // string, unlimited
192
    };
193
    /**
194
     * The data of the 'ds64' chunk.
195
     * Used only with RF64 files.
196
     * @type {!Object<string, *>}
197
     */
198
    this.ds64 = {
199
      /** @type {string} */
200
      chunkId: '',
201
      /** @type {number} */
202
      chunkSize: 0,
203
      /** @type {number} */
204
      riffSizeHigh: 0, // DWORD
205
      /** @type {number} */
206
      riffSizeLow: 0, // DWORD
207
      /** @type {number} */
208
      dataSizeHigh: 0, // DWORD
209
      /** @type {number} */
210
      dataSizeLow: 0, // DWORD
211
      /** @type {number} */
212
      originationTime: 0, // DWORD
213
      /** @type {number} */
214
      sampleCountHigh: 0, // DWORD
215
      /** @type {number} */
216
      sampleCountLow: 0 // DWORD
217
      /** @type {number} */
218
      //'tableLength': 0, // DWORD
219
      /** @type {!Array<number>} */
220
      //'table': []
221
    };
222
    /**
223
     * The data of the 'data' chunk.
224
     * @type {!Object<string, *>}
225
     */
226
    this.data = {
227
      /** @type {string} */
228
      chunkId: '',
229
      /** @type {number} */
230
      chunkSize: 0,
231
      /** @type {!Uint8Array} */
232
      samples: new Uint8Array(0)
233
    };
234
    /**
235
     * The data of the 'LIST' chunks.
236
     * Each item in this list look like this:
237
     *  {
238
     *      chunkId: '',
239
     *      chunkSize: 0,
240
     *      format: '',
241
     *      subChunks: []
242
     *   }
243
     * @type {!Array<!Object>}
244
     */
245
    this.LIST = [];
246
    /**
247
     * The data of the 'junk' chunk.
248
     * @type {!Object<string, *>}
249
     */
250
    this.junk = {
251
      /** @type {string} */
252
      chunkId: '',
253
      /** @type {number} */
254
      chunkSize: 0,
255
      /** @type {!Array<number>} */
256
      chunkData: []
257
    };
258
    /**
259
     * The bit depth code according to the samples.
260
     * @type {string}
261
     */
262
    this.bitDepth = '0';
263
    /**
264
     * @type {!Object}
265
     * @private
266
     */
267
    this.dataType = {};
268
  }
269
270
  /**
271
   * Set up the WaveFileParser object from a byte buffer.
272
   * @param {!Uint8Array} wavBuffer The buffer.
273
   * @param {boolean=} samples True if the samples should be loaded.
274
   * @throws {Error} If container is not RIFF, RIFX or RF64.
275
   * @throws {Error} If format is not WAVE.
276
   * @throws {Error} If no 'fmt ' chunk is found.
277
   * @throws {Error} If no 'data' chunk is found.
278
   * @protected
279
   */
280
  readBuffer(wavBuffer, samples=true) {
281
    this.clearHeader_();
282
    this.head_ = 0;
283
    this.readRIFFChunk(wavBuffer);
284
    if (this.format != 'WAVE') {
285
      throw Error('Could not find the "WAVE" format identifier');
286
    }
287
    this.setSignature(wavBuffer);
288
    this.readDs64Chunk_(wavBuffer);
289
    this.readFmtChunk_(wavBuffer);
290
    this.readFactChunk_(wavBuffer);
291
    this.readBextChunk_(wavBuffer);
292
    this.readCueChunk_(wavBuffer);
293
    this.readSmplChunk_(wavBuffer);
294
    this.readDataChunk_(wavBuffer, samples);
295
    this.readJunkChunk_(wavBuffer);
296
    this.readLISTChunk_(wavBuffer);
297
    this.bitDepthFromFmt_();
298
    this.updateDataType_();
299
  }
300
301
  /**
302
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
303
   * The return value of this method can be written straight to disk.
304
   * @return {!Uint8Array} A wav file.
305
   * @throws {Error} If bit depth is invalid.
306
   * @throws {Error} If the number of channels is invalid.
307
   * @throws {Error} If the sample rate is invalid.
308
   * @protected
309
   */
310
  writeBuffer() {
311
    this.validateWavHeader_();
312
    return this.writeWavBuffer_();
313
  }
314
315
  /**
316
   * Set the string code of the bit depth based on the 'fmt ' chunk.
317
   * @private
318
   */
319
  bitDepthFromFmt_() {
320
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
321
      this.bitDepth = '32f';
322
    } else if (this.fmt.audioFormat === 6) {
323
      this.bitDepth = '8a';
324
    } else if (this.fmt.audioFormat === 7) {
325
      this.bitDepth = '8m';
326
    } else {
327
      this.bitDepth = this.fmt.bitsPerSample.toString();
328
    }
329
  }
330
331
  /**
332
   * Reset some attributes of the object.
333
   * @private
334
   */
335
  clearHeader_() {
336
    this.fmt.cbSize = 0;
337
    this.fmt.validBitsPerSample = 0;
338
    this.fact.chunkId = '';
339
    this.ds64.chunkId = '';
340
  }
341
342
  /**
343
   * Update the type definition used to read and write the samples.
344
   * @private
345
   */
346
  updateDataType_() {
347
    this.dataType = {
348
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
349
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
350
      signed: this.bitDepth != '8',
351
      be: this.container == 'RIFX'
352
    };
353
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
354
      this.dataType.bits = 8;
355
      this.dataType.signed = false;
356
    }
357
  }
358
359
  /**
360
   * Return a .wav file byte buffer with the data from the WaveFileParser object.
361
   * The return value of this method can be written straight to disk.
362
   * @return {!Uint8Array} The wav file bytes.
363
   * @private
364
   */
365
  writeWavBuffer_() {
366
    this.uInt16_.be = this.container === 'RIFX';
367
    this.uInt32_.be = this.uInt16_.be;
368
    /** @type {!Array<!Array<number>>} */
369
    let fileBody = [
370
      this.getJunkBytes_(),
371
      this.getDs64Bytes_(),
372
      this.getBextBytes_(),
373
      this.getFmtBytes_(),
374
      this.getFactBytes_(),
375
      packString(this.data.chunkId),
376
      pack(this.data.samples.length, this.uInt32_),
377
      this.data.samples,
378
      this.getCueBytes_(),
379
      this.getSmplBytes_(),
380
      this.getLISTBytes_()
381
    ];
382
    /** @type {number} */
383
    let fileBodyLength = 0;
384
    for (let i=0; i<fileBody.length; i++) {
385
      fileBodyLength += fileBody[i].length;
386
    }
387
    /** @type {!Uint8Array} */
388
    let file = new Uint8Array(fileBodyLength + 12);
389
    /** @type {number} */
390
    let index = 0;
391
    index = packStringTo(this.container, file, index);
392
    index = packTo(fileBodyLength + 4, this.uInt32_, file, index);
393
    index = packStringTo(this.format, file, index);
394
    for (let i=0; i<fileBody.length; i++) {
395
      file.set(fileBody[i], index);
396
      index += fileBody[i].length;
397
    }
398
    return file;
399
  }
400
401
  /**
402
   * Return the bytes of the 'bext' chunk.
403
   * @private
404
   */
405
  getBextBytes_() {
406
    /** @type {!Array<number>} */
407
    let bytes = [];
408
    this.enforceBext_();
409
    if (this.bext.chunkId) {
410
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
411
      bytes = bytes.concat(
412
        packString(this.bext.chunkId),
413
        pack(602 + this.bext.codingHistory.length, this.uInt32_),
414
        writeString(this.bext.description, 256),
415
        writeString(this.bext.originator, 32),
416
        writeString(this.bext.originatorReference, 32),
417
        writeString(this.bext.originationDate, 10),
418
        writeString(this.bext.originationTime, 8),
419
        pack(this.bext.timeReference[0], this.uInt32_),
420
        pack(this.bext.timeReference[1], this.uInt32_),
421
        pack(this.bext.version, this.uInt16_),
422
        writeString(this.bext.UMID, 64),
423
        pack(this.bext.loudnessValue, this.uInt16_),
424
        pack(this.bext.loudnessRange, this.uInt16_),
425
        pack(this.bext.maxTruePeakLevel, this.uInt16_),
426
        pack(this.bext.maxMomentaryLoudness, this.uInt16_),
427
        pack(this.bext.maxShortTermLoudness, this.uInt16_),
428
        writeString(this.bext.reserved, 180),
429
        writeString(
430
          this.bext.codingHistory, this.bext.codingHistory.length));
431
    }
432
    return bytes;
433
  }
434
435
  /**
436
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
437
   * @private
438
   */
439
  enforceBext_() {
440
    for (let prop in this.bext) {
441
      if (this.bext.hasOwnProperty(prop)) {
442
        if (this.bext[prop] && prop != 'timeReference') {
443
          this.bext.chunkId = 'bext';
444
          break;
445
        }
446
      }
447
    }
448
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
449
      this.bext.chunkId = 'bext';
450
    }
451
  }
452
453
  /**
454
   * Return the bytes of the 'ds64' chunk.
455
   * @return {!Array<number>} The 'ds64' chunk bytes.
456
   * @private
457
   */
458
  getDs64Bytes_() {
459
    /** @type {!Array<number>} */
460
    let bytes = [];
461
    if (this.ds64.chunkId) {
462
      bytes = bytes.concat(
463
        packString(this.ds64.chunkId),
464
        pack(this.ds64.chunkSize, this.uInt32_),
465
        pack(this.ds64.riffSizeHigh, this.uInt32_),
466
        pack(this.ds64.riffSizeLow, this.uInt32_),
467
        pack(this.ds64.dataSizeHigh, this.uInt32_),
468
        pack(this.ds64.dataSizeLow, this.uInt32_),
469
        pack(this.ds64.originationTime, this.uInt32_),
470
        pack(this.ds64.sampleCountHigh, this.uInt32_),
471
        pack(this.ds64.sampleCountLow, this.uInt32_));
472
    }
473
    //if (this.ds64.tableLength) {
474
    //  ds64Bytes = ds64Bytes.concat(
475
    //    pack(this.ds64.tableLength, this.uInt32_),
476
    //    this.ds64.table);
477
    //}
478
    return bytes;
479
  }
480
481
  /**
482
   * Return the bytes of the 'cue ' chunk.
483
   * @return {!Array<number>} The 'cue ' chunk bytes.
484
   * @private
485
   */
486
  getCueBytes_() {
487
    /** @type {!Array<number>} */
488
    let bytes = [];
489
    if (this.cue.chunkId) {
490
      /** @type {!Array<number>} */
491
      let cuePointsBytes = this.getCuePointsBytes_();
492
      bytes = bytes.concat(
493
        packString(this.cue.chunkId),
494
        pack(cuePointsBytes.length + 4, this.uInt32_),
495
        pack(this.cue.dwCuePoints, this.uInt32_),
496
        cuePointsBytes);
497
    }
498
    return bytes;
499
  }
500
501
  /**
502
   * Return the bytes of the 'cue ' points.
503
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
504
   * @private
505
   */
506
  getCuePointsBytes_() {
507
    /** @type {!Array<number>} */
508
    let points = [];
509
    for (let i=0; i<this.cue.dwCuePoints; i++) {
510
      points = points.concat(
511
        pack(this.cue.points[i].dwName, this.uInt32_),
512
        pack(this.cue.points[i].dwPosition, this.uInt32_),
513
        packString(this.cue.points[i].fccChunk),
514
        pack(this.cue.points[i].dwChunkStart, this.uInt32_),
515
        pack(this.cue.points[i].dwBlockStart, this.uInt32_),
516
        pack(this.cue.points[i].dwSampleOffset, this.uInt32_));
517
    }
518
    return points;
519
  }
520
521
  /**
522
   * Return the bytes of the 'smpl' chunk.
523
   * @return {!Array<number>} The 'smpl' chunk bytes.
524
   * @private
525
   */
526
  getSmplBytes_() {
527
    /** @type {!Array<number>} */
528
    let bytes = [];
529
    if (this.smpl.chunkId) {
530
      /** @type {!Array<number>} */
531
      let smplLoopsBytes = this.getSmplLoopsBytes_();
532
      bytes = bytes.concat(
533
        packString(this.smpl.chunkId),
534
        pack(smplLoopsBytes.length + 36, this.uInt32_),
535
        pack(this.smpl.dwManufacturer, this.uInt32_),
536
        pack(this.smpl.dwProduct, this.uInt32_),
537
        pack(this.smpl.dwSamplePeriod, this.uInt32_),
538
        pack(this.smpl.dwMIDIUnityNote, this.uInt32_),
539
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32_),
540
        pack(this.smpl.dwSMPTEFormat, this.uInt32_),
541
        pack(this.smpl.dwSMPTEOffset, this.uInt32_),
542
        pack(this.smpl.dwNumSampleLoops, this.uInt32_),
543
        pack(this.smpl.dwSamplerData, this.uInt32_),
544
        smplLoopsBytes);
545
    }
546
    return bytes;
547
  }
548
549
  /**
550
   * Return the bytes of the 'smpl' loops.
551
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
552
   * @private
553
   */
554
  getSmplLoopsBytes_() {
555
    /** @type {!Array<number>} */
556
    let loops = [];
557
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
558
      loops = loops.concat(
559
        pack(this.smpl.loops[i].dwName, this.uInt32_),
560
        pack(this.smpl.loops[i].dwType, this.uInt32_),
561
        pack(this.smpl.loops[i].dwStart, this.uInt32_),
562
        pack(this.smpl.loops[i].dwEnd, this.uInt32_),
563
        pack(this.smpl.loops[i].dwFraction, this.uInt32_),
564
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32_));
565
    }
566
    return loops;
567
  }
568
569
  /**
570
   * Return the bytes of the 'fact' chunk.
571
   * @return {!Array<number>} The 'fact' chunk bytes.
572
   * @private
573
   */
574
  getFactBytes_() {
575
    /** @type {!Array<number>} */
576
    let bytes = [];
577
    if (this.fact.chunkId) {
578
      bytes = bytes.concat(
579
        packString(this.fact.chunkId),
580
        pack(this.fact.chunkSize, this.uInt32_),
581
        pack(this.fact.dwSampleLength, this.uInt32_));
582
    }
583
    return bytes;
584
  }
585
586
  /**
587
   * Return the bytes of the 'fmt ' chunk.
588
   * @return {!Array<number>} The 'fmt' chunk bytes.
589
   * @throws {Error} if no 'fmt ' chunk is present.
590
   * @private
591
   */
592
  getFmtBytes_() {
593
    /** @type {!Array<number>} */
594
    let fmtBytes = [];
595
    if (this.fmt.chunkId) {
596
      return fmtBytes.concat(
597
        packString(this.fmt.chunkId),
598
        pack(this.fmt.chunkSize, this.uInt32_),
599
        pack(this.fmt.audioFormat, this.uInt16_),
600
        pack(this.fmt.numChannels, this.uInt16_),
601
        pack(this.fmt.sampleRate, this.uInt32_),
602
        pack(this.fmt.byteRate, this.uInt32_),
603
        pack(this.fmt.blockAlign, this.uInt16_),
604
        pack(this.fmt.bitsPerSample, this.uInt16_),
605
        this.getFmtExtensionBytes_());
606
    }
607
    throw Error('Could not find the "fmt " chunk');
608
  }
609
610
  /**
611
   * Return the bytes of the fmt extension fields.
612
   * @return {!Array<number>} The fmt extension bytes.
613
   * @private
614
   */
615
  getFmtExtensionBytes_() {
616
    /** @type {!Array<number>} */
617
    let extension = [];
618
    if (this.fmt.chunkSize > 16) {
619
      extension = extension.concat(
620
        pack(this.fmt.cbSize, this.uInt16_));
621
    }
622
    if (this.fmt.chunkSize > 18) {
623
      extension = extension.concat(
624
        pack(this.fmt.validBitsPerSample, this.uInt16_));
625
    }
626
    if (this.fmt.chunkSize > 20) {
627
      extension = extension.concat(
628
        pack(this.fmt.dwChannelMask, this.uInt32_));
629
    }
630
    if (this.fmt.chunkSize > 24) {
631
      extension = extension.concat(
632
        pack(this.fmt.subformat[0], this.uInt32_),
633
        pack(this.fmt.subformat[1], this.uInt32_),
634
        pack(this.fmt.subformat[2], this.uInt32_),
635
        pack(this.fmt.subformat[3], this.uInt32_));
636
    }
637
    return extension;
638
  }
639
640
  /**
641
   * Return the bytes of the 'LIST' chunk.
642
   * @return {!Array<number>} The 'LIST' chunk bytes.
643
   * @private
644
   */
645
  getLISTBytes_() {
646
    /** @type {!Array<number>} */
647
    let bytes = [];
648
    for (let i=0; i<this.LIST.length; i++) {
649
      /** @type {!Array<number>} */
650
      let subChunksBytes = this.getLISTSubChunksBytes_(
651
          this.LIST[i].subChunks, this.LIST[i].format);
652
      bytes = bytes.concat(
653
        packString(this.LIST[i].chunkId),
654
        pack(subChunksBytes.length + 4, this.uInt32_),
655
        packString(this.LIST[i].format),
656
        subChunksBytes);
657
    }
658
    return bytes;
659
  }
660
661
  /**
662
   * Return the bytes of the sub chunks of a 'LIST' chunk.
663
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
664
   * @param {string} format The format of the 'LIST' chunk.
665
   *    Currently supported values are 'adtl' or 'INFO'.
666
   * @return {!Array<number>} The sub chunk bytes.
667
   * @private
668
   */
669
  getLISTSubChunksBytes_(subChunks, format) {
670
    /** @type {!Array<number>} */
671
    let bytes = [];
672
    for (let i=0; i<subChunks.length; i++) {
673
      if (format == 'INFO') {
674
        bytes = bytes.concat(
675
          packString(subChunks[i].chunkId),
676
          pack(subChunks[i].value.length + 1, this.uInt32_),
677
          writeString(
678
            subChunks[i].value, subChunks[i].value.length));
679
        bytes.push(0);
680
      } else if (format == 'adtl') {
681
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
682
          bytes = bytes.concat(
683
            packString(subChunks[i].chunkId),
684
            pack(
685
              subChunks[i].value.length + 4 + 1, this.uInt32_),
686
            pack(subChunks[i].dwName, this.uInt32_),
687
            writeString(
688
              subChunks[i].value,
689
              subChunks[i].value.length));
690
          bytes.push(0);
691
        } else if (subChunks[i].chunkId == 'ltxt') {
692
          bytes = bytes.concat(
693
            this.getLtxtChunkBytes_(subChunks[i]));
694
        }
695
      }
696
      if (bytes.length % 2) {
697
        bytes.push(0);
698
      }
699
    }
700
    return bytes;
701
  }
702
703
  /**
704
   * Return the bytes of a 'ltxt' chunk.
705
   * @param {!Object} ltxt the 'ltxt' chunk.
706
   * @private
707
   */
708
  getLtxtChunkBytes_(ltxt) {
709
    return [].concat(
710
      packString(ltxt.chunkId),
711
      pack(ltxt.value.length + 20, this.uInt32_),
712
      pack(ltxt.dwName, this.uInt32_),
713
      pack(ltxt.dwSampleLength, this.uInt32_),
714
      pack(ltxt.dwPurposeID, this.uInt32_),
715
      pack(ltxt.dwCountry, this.uInt16_),
716
      pack(ltxt.dwLanguage, this.uInt16_),
717
      pack(ltxt.dwDialect, this.uInt16_),
718
      pack(ltxt.dwCodePage, this.uInt16_),
719
      writeString(ltxt.value, ltxt.value.length));
720
  }
721
722
  /**
723
   * Return the bytes of the 'junk' chunk.
724
   * @private
725
   */
726
  getJunkBytes_() {
727
    /** @type {!Array<number>} */
728
    let bytes = [];
729
    if (this.junk.chunkId) {
730
      return bytes.concat(
731
        packString(this.junk.chunkId),
732
        pack(this.junk.chunkData.length, this.uInt32_),
733
        this.junk.chunkData);
734
    }
735
    return bytes;
736
  }
737
738
  /**
739
   * Read the 'fmt ' chunk of a wave file.
740
   * @param {!Uint8Array} buffer The wav file buffer.
741
   * @throws {Error} If no 'fmt ' chunk is found.
742
   * @private
743
   */
744
  readFmtChunk_(buffer) {
745
    /** @type {?Object} */
746
    let chunk = this.findChunk('fmt ');
747
    if (chunk) {
748
      this.head_ = chunk.chunkData.start;
749
      this.fmt.chunkId = chunk.chunkId;
750
      this.fmt.chunkSize = chunk.chunkSize;
751
      this.fmt.audioFormat = this.readNumber(buffer, this.uInt16_);
752
      this.fmt.numChannels = this.readNumber(buffer, this.uInt16_);
753
      this.fmt.sampleRate = this.readNumber(buffer, this.uInt32_);
754
      this.fmt.byteRate = this.readNumber(buffer, this.uInt32_);
755
      this.fmt.blockAlign = this.readNumber(buffer, this.uInt16_);
756
      this.fmt.bitsPerSample = this.readNumber(buffer, this.uInt16_);
757
      this.readFmtExtension_(buffer);
758
    } else {
759
      throw Error('Could not find the "fmt " chunk');
760
    }
761
  }
762
763
  /**
764
   * Read the 'fmt ' chunk extension.
765
   * @param {!Uint8Array} buffer The wav file buffer.
766
   * @private
767
   */
768
  readFmtExtension_(buffer) {
769
    if (this.fmt.chunkSize > 16) {
770
      this.fmt.cbSize = this.readNumber(buffer, this.uInt16_);
771
      if (this.fmt.chunkSize > 18) {
772
        this.fmt.validBitsPerSample = this.readNumber(buffer, this.uInt16_);
773
        if (this.fmt.chunkSize > 20) {
774
          this.fmt.dwChannelMask = this.readNumber(buffer, this.uInt32_);
775
          this.fmt.subformat = [
776
            this.readNumber(buffer, this.uInt32_),
777
            this.readNumber(buffer, this.uInt32_),
778
            this.readNumber(buffer, this.uInt32_),
779
            this.readNumber(buffer, this.uInt32_)];
780
        }
781
      }
782
    }
783
  }
784
785
  /**
786
   * Read the 'fact' chunk of a wav file.
787
   * @param {!Uint8Array} buffer The wav file buffer.
788
   * @private
789
   */
790
  readFactChunk_(buffer) {
791
    /** @type {?Object} */
792
    let chunk = this.findChunk('fact');
793
    if (chunk) {
794
      this.head_ = chunk.chunkData.start;
795
      this.fact.chunkId = chunk.chunkId;
796
      this.fact.chunkSize = chunk.chunkSize;
797
      this.fact.dwSampleLength = this.readNumber(buffer, this.uInt32_);
798
    }
799
  }
800
801
  /**
802
   * Read the 'cue ' chunk of a wave file.
803
   * @param {!Uint8Array} buffer The wav file buffer.
804
   * @private
805
   */
806
  readCueChunk_(buffer) {
807
    /** @type {?Object} */
808
    let chunk = this.findChunk('cue ');
809
    if (chunk) {
810
      this.head_ = chunk.chunkData.start;
811
      this.cue.chunkId = chunk.chunkId;
812
      this.cue.chunkSize = chunk.chunkSize;
813
      this.cue.dwCuePoints = this.readNumber(buffer, this.uInt32_);
814
      for (let i = 0; i < this.cue.dwCuePoints; i++) {
815
        this.cue.points.push({
816
          dwName: this.readNumber(buffer, this.uInt32_),
817
          dwPosition: this.readNumber(buffer, this.uInt32_),
818
          fccChunk: this.readString(buffer, 4),
819
          dwChunkStart: this.readNumber(buffer, this.uInt32_),
820
          dwBlockStart: this.readNumber(buffer, this.uInt32_),
821
          dwSampleOffset: this.readNumber(buffer, this.uInt32_),
822
        });
823
      }
824
    }
825
  }
826
827
  /**
828
   * Read the 'smpl' chunk of a wave file.
829
   * @param {!Uint8Array} buffer The wav file buffer.
830
   * @private
831
   */
832
  readSmplChunk_(buffer) {
833
    /** @type {?Object} */
834
    let chunk = this.findChunk('smpl');
835
    if (chunk) {
836
      this.head_ = chunk.chunkData.start;
837
      this.smpl.chunkId = chunk.chunkId;
838
      this.smpl.chunkSize = chunk.chunkSize;
839
      this.smpl.dwManufacturer = this.readNumber(buffer, this.uInt32_);
840
      this.smpl.dwProduct = this.readNumber(buffer, this.uInt32_);
841
      this.smpl.dwSamplePeriod = this.readNumber(buffer, this.uInt32_);
842
      this.smpl.dwMIDIUnityNote = this.readNumber(buffer, this.uInt32_);
843
      this.smpl.dwMIDIPitchFraction = this.readNumber(buffer, this.uInt32_);
844
      this.smpl.dwSMPTEFormat = this.readNumber(buffer, this.uInt32_);
845
      this.smpl.dwSMPTEOffset = this.readNumber(buffer, this.uInt32_);
846
      this.smpl.dwNumSampleLoops = this.readNumber(buffer, this.uInt32_);
847
      this.smpl.dwSamplerData = this.readNumber(buffer, this.uInt32_);
848
      for (let i = 0; i < this.smpl.dwNumSampleLoops; i++) {
849
        this.smpl.loops.push({
850
          dwName: this.readNumber(buffer, this.uInt32_),
851
          dwType: this.readNumber(buffer, this.uInt32_),
852
          dwStart: this.readNumber(buffer, this.uInt32_),
853
          dwEnd: this.readNumber(buffer, this.uInt32_),
854
          dwFraction: this.readNumber(buffer, this.uInt32_),
855
          dwPlayCount: this.readNumber(buffer, this.uInt32_),
856
        });
857
      }
858
    }
859
  }
860
861
  /**
862
   * Read the 'data' chunk of a wave file.
863
   * @param {!Uint8Array} buffer The wav file buffer.
864
   * @param {boolean} samples True if the samples should be loaded.
865
   * @throws {Error} If no 'data' chunk is found.
866
   * @private
867
   */
868
  readDataChunk_(buffer, samples) {
869
    /** @type {?Object} */
870
    let chunk = this.findChunk('data');
871
    if (chunk) {
872
      this.data.chunkId = 'data';
873
      this.data.chunkSize = chunk.chunkSize;
874
      if (samples) {
875
        this.data.samples = buffer.slice(
876
          chunk.chunkData.start,
877
          chunk.chunkData.end);
878
      }
879
    } else {
880
      throw Error('Could not find the "data" chunk');
881
    }
882
  }
883
884
  /**
885
   * Read the 'bext' chunk of a wav file.
886
   * @param {!Uint8Array} buffer The wav file buffer.
887
   * @private
888
   */
889
  readBextChunk_(buffer) {
890
    /** @type {?Object} */
891
    let chunk = this.findChunk('bext');
892
    if (chunk) {
893
      this.head_ = chunk.chunkData.start;
894
      this.bext.chunkId = chunk.chunkId;
895
      this.bext.chunkSize = chunk.chunkSize;
896
      this.bext.description = this.readString(buffer, 256);
897
      this.bext.originator = this.readString(buffer, 32);
898
      this.bext.originatorReference = this.readString(buffer, 32);
899
      this.bext.originationDate = this.readString(buffer, 10);
900
      this.bext.originationTime = this.readString(buffer, 8);
901
      this.bext.timeReference = [
902
        this.readNumber(buffer, this.uInt32_),
903
        this.readNumber(buffer, this.uInt32_)];
904
      this.bext.version = this.readNumber(buffer, this.uInt16_);
905
      this.bext.UMID = this.readString(buffer, 64);
906
      this.bext.loudnessValue = this.readNumber(buffer, this.uInt16_);
907
      this.bext.loudnessRange = this.readNumber(buffer, this.uInt16_);
908
      this.bext.maxTruePeakLevel = this.readNumber(buffer, this.uInt16_);
909
      this.bext.maxMomentaryLoudness = this.readNumber(buffer, this.uInt16_);
910
      this.bext.maxShortTermLoudness = this.readNumber(buffer, this.uInt16_);
911
      this.bext.reserved = this.readString(buffer, 180);
912
      this.bext.codingHistory = this.readString(
913
        buffer, this.bext.chunkSize - 602);
914
    }
915
  }
916
917
  /**
918
   * Read the 'ds64' chunk of a wave file.
919
   * @param {!Uint8Array} buffer The wav file buffer.
920
   * @throws {Error} If no 'ds64' chunk is found and the file is RF64.
921
   * @private
922
   */
923
  readDs64Chunk_(buffer) {
924
    /** @type {?Object} */
925
    let chunk = this.findChunk('ds64');
926
    if (chunk) {
927
      this.head_ = chunk.chunkData.start;
928
      this.ds64.chunkId = chunk.chunkId;
929
      this.ds64.chunkSize = chunk.chunkSize;
930
      this.ds64.riffSizeHigh = this.readNumber(buffer, this.uInt32_);
931
      this.ds64.riffSizeLow = this.readNumber(buffer, this.uInt32_);
932
      this.ds64.dataSizeHigh = this.readNumber(buffer, this.uInt32_);
933
      this.ds64.dataSizeLow = this.readNumber(buffer, this.uInt32_);
934
      this.ds64.originationTime = this.readNumber(buffer, this.uInt32_);
935
      this.ds64.sampleCountHigh = this.readNumber(buffer, this.uInt32_);
936
      this.ds64.sampleCountLow = this.readNumber(buffer, this.uInt32_);
937
      //if (wav.ds64.chunkSize > 28) {
938
      //  wav.ds64.tableLength = unpack(
939
      //    chunkData.slice(28, 32), uInt32_);
940
      //  wav.ds64.table = chunkData.slice(
941
      //     32, 32 + wav.ds64.tableLength);
942
      //}
943
    } else {
944
      if (this.container == 'RF64') {
945
        throw Error('Could not find the "ds64" chunk');
946
      }
947
    }
948
  }
949
950
  /**
951
   * Read the 'LIST' chunks of a wave file.
952
   * @param {!Uint8Array} buffer The wav file buffer.
953
   * @private
954
   */
955
  readLISTChunk_(buffer) {
956
    /** @type {?Object} */
957
    let listChunks = this.findChunk('LIST', true);
958
    if (listChunks !== null) {
959
      for (let j=0; j < listChunks.length; j++) {
960
        /** @type {!Object} */
961
        let subChunk = listChunks[j];
962
        this.LIST.push({
963
          chunkId: subChunk.chunkId,
964
          chunkSize: subChunk.chunkSize,
965
          format: subChunk.format,
966
          subChunks: []});
967
        for (let x=0; x<subChunk.subChunks.length; x++) {
968
          this.readLISTSubChunks_(subChunk.subChunks[x],
969
            subChunk.format, buffer);
970
        }
971
      }
972
    }
973
  }
974
975
  /**
976
   * Read the sub chunks of a 'LIST' chunk.
977
   * @param {!Object} subChunk The 'LIST' subchunks.
978
   * @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
979
   * @param {!Uint8Array} buffer The wav file buffer.
980
   * @private
981
   */
982
  readLISTSubChunks_(subChunk, format, buffer) {
983
    if (format == 'adtl') {
984
      if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
985
        this.head_ = subChunk.chunkData.start;
986
        /** @type {!Object<string, string|number>} */
987
        let item = {
988
          chunkId: subChunk.chunkId,
989
          chunkSize: subChunk.chunkSize,
990
          dwName: this.readNumber(buffer, this.uInt32_)
991
        };
992
        if (subChunk.chunkId == 'ltxt') {
993
          item.dwSampleLength = this.readNumber(buffer, this.uInt32_);
994
          item.dwPurposeID = this.readNumber(buffer, this.uInt32_);
995
          item.dwCountry = this.readNumber(buffer, this.uInt16_);
996
          item.dwLanguage = this.readNumber(buffer, this.uInt16_);
997
          item.dwDialect = this.readNumber(buffer, this.uInt16_);
998
          item.dwCodePage = this.readNumber(buffer, this.uInt16_);
999
        }
1000
        item.value = this.readZSTR(buffer, this.head_);
1001
        this.LIST[this.LIST.length - 1].subChunks.push(item);
1002
      }
1003
    // RIFF INFO tags like ICRD, ISFT, ICMT
1004
    } else if(format == 'INFO') {
1005
      this.head_ = subChunk.chunkData.start;
1006
      this.LIST[this.LIST.length - 1].subChunks.push({
1007
        chunkId: subChunk.chunkId,
1008
        chunkSize: subChunk.chunkSize,
1009
        value: this.readZSTR(buffer, this.head_)
1010
      });
1011
    }
1012
  }
1013
1014
  /**
1015
   * Read the 'junk' chunk of a wave file.
1016
   * @param {!Uint8Array} buffer The wav file buffer.
1017
   * @private
1018
   */
1019
  readJunkChunk_(buffer) {
1020
    /** @type {?Object} */
1021
    let chunk = this.findChunk('junk');
1022
    if (chunk) {
1023
      this.junk = {
1024
        chunkId: chunk.chunkId,
1025
        chunkSize: chunk.chunkSize,
1026
        chunkData: [].slice.call(buffer.slice(
1027
          chunk.chunkData.start,
1028
          chunk.chunkData.end))
1029
      };
1030
    }
1031
  }
1032
1033
  /**
1034
   * Validate the header of the file.
1035
   * @throws {Error} If bit depth is invalid.
1036
   * @throws {Error} If the number of channels is invalid.
1037
   * @throws {Error} If the sample rate is invalid.
1038
   * @private
1039
   */
1040
  validateWavHeader_() {
1041
    this.validateBitDepth_();
1042
    this.validateNumChannels_();
1043
    this.validateSampleRate_();
1044
  }
1045
1046
  /**
1047
   * Validate the bit depth.
1048
   * @return {boolean} True is the bit depth is valid.
1049
   * @throws {Error} If bit depth is invalid.
1050
   * @private
1051
   */
1052
  validateBitDepth_() {
1053
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
1054
      if (parseInt(this.bitDepth, 10) > 8 &&
1055
          parseInt(this.bitDepth, 10) < 54) {
1056
        return true;
1057
      }
1058
      throw new Error('Invalid bit depth.');
1059
    }
1060
    return true;
1061
  }
1062
1063
  /**
1064
   * Validate the number of channels.
1065
   * @return {boolean} True is the number of channels is valid.
1066
   * @throws {Error} If the number of channels is invalid.
1067
   * @private
1068
   */
1069
  validateNumChannels_() {
1070
    /** @type {number} */
1071
    let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
1072
    if (this.fmt.numChannels < 1 || blockAlign > 65535) {
1073
      throw new Error('Invalid number of channels.');
1074
    }
1075
    return true;
1076
  }
1077
1078
  /**
1079
   * Validate the sample rate value.
1080
   * @return {boolean} True is the sample rate is valid.
1081
   * @throws {Error} If the sample rate is invalid.
1082
   * @private
1083
   */
1084
  validateSampleRate_() {
1085
    /** @type {number} */
1086
    let byteRate = this.fmt.numChannels *
1087
      (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
1088
    if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
1089
      throw new Error('Invalid sample rate.');
1090
    }
1091
    return true;
1092
  }
1093
}
1094